/*
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is ST_EXT-S_UIMAST.
 *
 * The Initial Developer of the Original Code is
 * University of Rome, Tor Vergata
 * Portions created by the Initial Developer are Copyright (C) 2009
 * the Initial Developer. All Rights Reserved.
 */

package it.uniroma2.art.uimast.servlet;

import it.uniroma2.art.ontapi.*;
import it.uniroma2.art.ontapi.vocabulary.RDF;
import it.uniroma2.art.semanticturkey.SemanticTurkeyOperations;
import it.uniroma2.art.semanticturkey.plugin.extpts.CXSL;
import it.uniroma2.art.semanticturkey.plugin.extpts.InterfaceServiceServlet;
import it.uniroma2.art.semanticturkey.resources.Resources;
import it.uniroma2.art.semanticturkey.servlet.ServletUtilities;
import it.uniroma2.art.semanticturkey.vocabulary.SemAnnotVocab;
import it.uniroma2.art.uimast.impl.AnnotationProjection;

import java.io.*;
import java.net.URI;
import java.util.*;
import java.util.jar.JarFile;
import org.apache.commons.collections.iterators.ReverseListIterator;
import org.apache.uima.UIMAFramework;
import org.apache.uima.analysis_engine.AnalysisEngine;
import org.apache.uima.cas.*;
import org.apache.uima.pear.tools.*;
import org.apache.uima.resource.ResourceManager;
import org.apache.uima.resource.ResourceSpecifier;
import org.apache.uima.util.*;
import org.apache.xerces.dom.DocumentImpl;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * Provides a service to integrate UIMA inside Semantic Turkey.
 * 
 * @author <a href="mailto:manuel.fiorelli@gmail.com">Manuel Fiorelli</a>
 * 
 */
public class UimaIntegration extends InterfaceServiceServlet {

	/**
	 * Constructs a <code>UimaIntegration</code> object.
	 * 
	 * @param id 
	 *            the service identifier. It will be used to forward a request
	 *            from the presentation layer to this service.
	 */
	public UimaIntegration(String id) {
		servletUtilities = new ServletUtilities();
		
		// Stores the service identifier.
		this.id = id;
	}

	// the CXSLFactory and XMLData methods inherit their documentation comments
	// from InterfaceServiceServlet.

	public CXSL CXSLFactory() {
		return null;
	}

	public synchronized Document XMLData() {
		// Gets the kind of request from the presentation layer.
		String request = _oReq.getParameter("request");

		// Dispatches the request to the appropriate handler.
		
		if (request.equals("installPEAR")) { // installs a PEAR package
			String installDirString = _oReq.getParameter("installDir");
			String pearString = _oReq.getParameter("pear");
			URI installDir = URI.create(installDirString);
			URI pear = URI.create(pearString);
			return installPEAR(installDir, pear);
		} else if (request.equals("loadPEAR")) { // loads a PEAR package
			String packageDescriptorString = _oReq.getParameter("uri");
			URI packageDescriptor = URI.create(packageDescriptorString);
			return loadPEAR(packageDescriptor);
		} else if (request.equals("getComponentInfo")) { // gets informations about a component
			return getComponentInfo();
		} else if (request.equals("getCorpus")) { // gets the current corpus
			return getCorpus();
		} else if (request.equals("addDocument")) { // adds a document to the corpus
			String lang = _oReq.getParameter("lang");
			URI uri = URI.create(_oReq.getParameter("uri"));
			return addDocument(lang, uri);
		} else if (request.equals("removeDocuments")) { // removes a document from the corpus
			String id = _oReq.getParameter("id");
			String t[] = id.split(",");
			return removeDocuments(t);
		} else if (request.equals("runAE")) { // runs an Analysis Engine on some documents
			String lang = _oReq.getParameter("lang");
			String locators = _oReq.getParameter("locators");
			String text = _oReq.getParameter("text");
			return runAE(lang, locators, text);
		} else if (request.equals("dumpTS")) { // gets the current Analysis Engine type system
			return dumpTS();
		} else if (request.equals("exportAnnotations")) { // exports the annotations as a collection of CASes
			String projection = _oReq.getParameter("projection");
			String outputFolder = _oReq.getParameter("outputFolder");
			return exportAnnotations(URI.create(projection), URI
					.create(outputFolder));
		} else {
			return servletUtilities.documentError("Unhandled request!");
		}
	}

	/**
	 * Export the annotations in a collection of CASes. Since UIMA doesn't prescribe a format
	 * to store an annotated corpus, we decided to store each CAS in a single XMI file 
	 * containing the document URI and the associated metadata.
	 * 
	 * @param projection
	 *            the projection descriptor URI
	 * @param outputFolder
	 *            the output folder URI
	 * @return the response is {@link #ack()} if no error occurred, otherwise it
	 *         is {@link ServletUtilities#documentError(String)}
	 */
	private Document exportAnnotations(URI projection, URI outputFolder) {
		try {
			// Gets the projection descriptor file.
			File projectionFile = new File(projection);

			// Gets the output folder file (remember that in Unix-like systems
			// every object in a file system is treated as a kind of file).
			File outputFolderFile = new File(outputFolder);

			// Instantiates the projection (wrt the given descriptor).
			AnnotationProjection proj = new AnnotationProjection(projectionFile);

			// Gets the ontology repository interface.
			ARTRepository repository = Resources.getRepository();

			// Iterates over the annotated web pages (which are used as
			// subject of the ann:url property).
			Iterator<ARTResource> it = repository.listSTSubjectInstances(
					SemAnnotVocab.Res.url, null); // null identifies an unbound variable.

			// An hash set used to discard duplicated web pages.
			HashSet<ARTResource> webPages = new HashSet<ARTResource>();

			// Until there is another web page.
			while (it.hasNext()) {
				// Gets a web page
				ARTResource webPage = (ARTResource) it.next();

				// Adds the new web page to the hash set, to verify that it
				// wasn't already seen.
				if (webPages.add(webPage)) {

					// Applies the projection to the current web page.
					proj.apply(webPage);

					// Gets a file named {web page ID}.xmi
					File outFile = new File(outputFolderFile, repository.getQName(webPage.getURI())+ ".xmi");

					// Serializes the generated CAS to an XMI file.
					XmlCasSerializer.serialize(proj.getCAS(), new BufferedOutputStream(new FileOutputStream(outFile)));
				}
			}

			// If there wasn't any error, returns ack()
			return ack();
		} catch (Exception e) { // If there was an error
			e.printStackTrace();
			StringWriter w = new StringWriter();
			e.printStackTrace(new PrintWriter(w));
			
			return servletUtilities.documentError(w.toString());
		}
	}

	/**
	 * Installs a PEAR package.
	 * 
	 * @param installDir
	 *            the installation directory URI
	 * @param pear
	 *            the package URI
	 * @return the response is {@link #ack()} if no error occurred, otherwise it
	 *         is {@link ServletUtilities#documentError(String)}
	 */
	private Document installPEAR(URI installDir, URI pear) {
		// The package root directory.
		File rootDir = null;

		try {
			// The directory where every package will be installed.
			File installDirFile = new File(installDir);

			// The package.
			File pearFile = new File(pear);

			// Browses the package to install.
			PackageBrowser browser = new PackageBrowser(new JarFile(pearFile));

			// Gets the main component identifier.
			String componentID = browser.getInstallationDescriptor().getMainComponentId();

			// Gets the package installation root directory.
			// It is a directory inside installDirFile named as the component ID.
			rootDir = new File(installDirFile, componentID);

			// If the root directory exists, aborts the process.
			if (rootDir.exists())
				return servletUtilities.documentError("The PEAR package seems to be already installed.");

			// Otherwise, tries to install the PEAR package.
			PackageInstaller.installPackage(installDirFile, pearFile, true);

			// If the operation succeeded, returns ack()
			return ack();
		} catch (Exception e) { // if an error occurred, recursively deletes the
			// package root directory (if any)
			if (rootDir != null && rootDir.exists())
				delete(rootDir);
			e.printStackTrace();
			return servletUtilities.documentError(e.toString());
		}
	}

	/**
	 * Loads an installed pear package.
	 * 
	 * @param pearDir
	 *            the package root directory
	 * @return the response is {@link #ack()} if no error occurred, otherwise it
	 *         is {@link ServletUtilities#documentError(String)}
	 */
	private Document loadPEAR(URI pearDir) {
		try {
			// Browses the installed package.
			PackageBrowser browser = new PackageBrowser(new File(pearDir));

			// Gets the UIMA resource manager.
			ResourceManager resManager = UIMAFramework
					.newDefaultResourceManager();

			// Augments the class path.
			resManager.setExtensionClassPath(browser.buildComponentClassPath(),
					true);

			// Gets the main component descriptor.
			XMLInputSource in = new XMLInputSource(browser
					.getInstallationDescriptor().getMainComponentDesc());

			// Parses the component descriptor.
			ResourceSpecifier specifier = UIMAFramework.getXMLParser()
					.parseResourceSpecifier(in);

			// Gets the component ID.
			String t = browser.getInstallationDescriptor().getMainComponentId();

			// Generates a UUID which identifies the component instance.
			UUID uuid = UUID.randomUUID();

			// Regenerates the UUID until it is equal to the previous (if any).
			while (uuid.equals(instanceUUID))
				uuid = UUID.randomUUID();

			// Load the analysis engine.
			ae = UIMAFramework.produceAnalysisEngine(specifier, resManager,
					null);

			// Updates the component ID.
			componentID = t;

			// Updates the instance UUID.
			instanceUUID = uuid;

			// Returns an ack().
			return ack();
		} catch (Exception e) { // handles possible exceptions.
			e.printStackTrace();
			return servletUtilities.documentError(e.toString());
		}
	}

	/**
	 * Gets the currently loaded component informations.
	 * 
	 * @return the response is {@link #nack()} if there was an error, otherwise
	 *         is
	 *         <p>
	 *         <pre>
	 *         <Tree type="ack">
	 *         	<Component componentID="component ID" instanceUUID=" instance UUID" /> 
	 *         </Tree>
	 *         </pre>
	 */
	private Document getComponentInfo() {
		if (componentID != null) {	// Checks that there is a component.
			// Creates the response.
			Document ack = ack();
			
			// Gets the document element.
			Element root = ack.getDocumentElement();
			
			// Creates a <Component> element.
			Element c = ack.createElement("Component");
			
			// Stores the component ID.
			c.setAttribute("id", componentID);
			
			// Stores the instance UUID.
			c.setAttribute("instanceUUID", instanceUUID.toString());
			
			// Appends the <Component> element.
			root.appendChild(c);
			
			// Returns the response.
			return ack;
		} else {
			return nack();
		}
	}

	/**
	 * Produces an ack (acknowledge). <pre><Tree type="ack" /></pre>
	 * 
	 * @return an ack
	 */
	private Document ack() {
		// Creates a new document.
		Document xml = new DocumentImpl();
		
		// Creates the <Tree> element.
		Element root = xml.createElement("Tree");
		
		// Assigns "ack" to the "type" attribute.
		root.setAttribute("type", "ack");
		
		// Sets the document element.
		xml.appendChild(root);
		
		// Returns the response.
		return xml;
	}

	/**
	 * Produces a nack (negative acknowledge). <pre><Tree type="nack" /></pre>
	 * 
	 * @return a nack
	 */
	private Document nack() {
		// Creates a new DOM document.
		Document xml = new DocumentImpl();
		
		// Creates the <Tree> element.
		Element root = xml.createElement("Tree");
		
		// Assigns the value "nack" to the attribute "type".
		root.setAttribute("type", "nack");
		
		// Sets the document element.
		xml.appendChild(root);
	
		// Returns the document.
		return xml;
	}

	/**
	 * Deletes a file. Also, it can delete a non empty directory.
	 * 
	 * @param file
	 *            a file to delete
	 */
	private void delete(File file) {
		// Returns if the directory doesn't exist.
		if (!file.exists())
			return;

		// If the file is a directory, deletes its content.
		if (file.isDirectory()) {
			File afile[];
			//// Iterates over the current file's children.
			int j = (afile = file.listFiles()).length;
			for (int i = 0; i < j; i++) {
				File item = afile[i];
				delete(item);
			}

		}

		// Delete the current file.
		file.delete();
	}

	/**
	 * Gets the corpus. This method should never fail.
	 * 
	 * @return <pre>
	 *         <Tree type="ack">
	 *         	... 
	 *         	<Document id="document id" lang="document language" uri="document uri" />
	 *         	... 
	 *         </Tree>
	 *         </pre>
	 */
	private Document getCorpus() {
		// Initializes the corpus vocabulary.
		// TODO: remove this operation, when the application level is extensible
		try {
			CorpusVocabulary.Res.initialize();
		} catch (Exception e) { // The no fail guarantee forces us to ignore
			// this error (which is likely not to happen)
			return ack();
		}

		// Gets the ontology repository interface.
		ARTRepository repository = Resources.getRepository();

		// Creates an ack, which must be augmented.
		Document xml = ack();

		// Gets the document element of the ack document.
		Element root = xml.getDocumentElement();

		// An element representing a document.
		Element docNode;

		// Iterates over the documents in the corpus.
		for (ARTStatementIterator docIt = repository.getStatements(null,
				RDF.Res.TYPE, CorpusVocabulary.Res.DOCUMENT); docIt.hasNext();) {
			// Gets a document.
			ARTResource doc = docIt.next().getSubject();

			// Creates a <Document> element.
			docNode = xml.createElement("Document");

			// Stores the document identifier.
			docNode.setAttribute("id", doc.getLocalName());

			// Stores the document language.
			docNode.setAttribute("lang", ((ARTLiteral) repository
					.listSTDatatypePropertyValues(doc,
							CorpusVocabulary.Res.LANGUAGE).next()).getLabel());

			// Stores the document URI.
			docNode.setAttribute("uri",
					((ARTLiteral) repository.listSTDatatypePropertyValues(doc,
							CorpusVocabulary.Res.URI).next()).getLabel());

			// Appends the <Document> element to the response root.
			root.appendChild(docNode);
		}

		// Returns the response.
		return xml;
	}

	/**
	 * Adds a document to the corpus.
	 * 
	 * @param lang
	 *            the document language
	 * @param uri
	 *            the document URI
	 * @return <pre>
	 *         <Tree type="ack">
	 *         	<NewDocument id="document id" lang="document language" uri="document uri" /> 
	 *         </Tree>
	 *         </pre>
	 *         <p>
	 *         if an error occurred, return
	 *         {@link ServletUtilities#documentError(String)}
	 */
	private Document addDocument(String lang, URI uri) {
		// Initializes the corpus vocabulary.
		// TODO: remove this operation, when the application level is
		// extensible.
		try {
			CorpusVocabulary.Res.initialize();
		} catch (Exception e) {
			return servletUtilities.documentError(e.getMessage());
		}
		// Normalizes the document URI.
		String uriString = uri.normalize().toString();

		// Gets the ontology repository interface.
		ARTRepository repository = Resources.getRepository();

		// Creates an ARTLiteral representing the document URI.
		ARTLiteral uriLiteral = repository.createLiteral(uriString);

		// Iterates over the Document instances, to avoid duplicates.
		for (ARTStatementIterator it = repository.getStatements(null,
				CorpusVocabulary.Res.URI, uriLiteral); it.hasNext();)
			if (repository.hasType(it.next().getSubject(),
					CorpusVocabulary.Res.DOCUMENT))
				return servletUtilities
						.documentError("The document is already in the corpus");

		// Creates the document ID.
		String docID = repository.getDefaultNamespace()
				+ SemanticTurkeyOperations
						.generateNewSemanticAnnotationUUID(repository);

		try {
			// Adds an individual of the class Document.
			repository.addSTIndividual(docID, CorpusVocabulary.Res.DOCUMENT);

			// Gets the newly created individual.
			ARTResource doc = repository.createURIResource(docID);

			// Sets its uri.
			repository.instanciateDatatypeProperty(doc,
					CorpusVocabulary.Res.URI, uriString);

			// Sets its language.
			repository.instanciateDatatypeProperty(doc,
					CorpusVocabulary.Res.LANGUAGE, lang);
		} catch (Exception e) { // if an error occurred
			return servletUtilities.documentError(e.toString());
		}

		// Creates the response.
		Document xml = ack();
		Element root = xml.getDocumentElement();
		Element newDocument = xml.createElement("NewDocument");
		newDocument.setAttribute("id", docID);
		newDocument.setAttribute("lang", lang);
		newDocument.setAttribute("uri", uriString);
		root.appendChild(newDocument);

		return xml; // Returns the response.
	}

	/**
	 * Removes the given documents from the corpus.
	 * 
	 * @param docIDS
	 *            the documents identifier
	 * @return <i>unspecified</i>
	 */
	private Document removeDocuments(String docIDS[]) {
		// TODO: documents in the corpus cannot be removed
		return servletUtilities.documentError("Operation not yet implemented");
	}

	/**
	 * Runs a previously loaded analysis engine.
	 * 
	 * @deprecated
	 */
	private Document runAE(String lang, String locators, String text) {
		// TODO: update this method in order to accept a serialization of the
		// document DOM, which can be deserialized in the middle layer.

		try {
			if (ae == null) // checks if an Analysis Engine is loaded.
				return servletUtilities
						.documentError("No analysis engine loaded");

			// Creates the CAS which the analysis engine should work on.
			CAS cas = ae.newCAS();

			// Sets the document language.
			cas.setDocumentLanguage(lang);

			// Sets the document text.
			cas.setDocumentText(text);

			// Processes the document.
			ae.process(cas);

			// Returns an ack.
			return ack();
		} catch (Exception e) { // if an error occurred.
			e.printStackTrace();
			return servletUtilities.documentError(e.toString());
		}
	}

	/**
	 * Dumps the current analysis engine type system.
	 * 
	 * @return <pre>
	 *         <Tree type="ack">
	 *         	<Type name="type name"> 
	 *         		... subtypes ...
	 *         	</Type> 
	 *         </Tree>
	 *         </pre>
	 *         <p>
	 *         If an error occurred, returns
	 *         {@link ServletUtilities#documentError(String)}
	 */
	private Document dumpTS() {
		try {
			if (ae == null) // checks if there is an analysis engine.
				return servletUtilities
						.documentError("No analysis engine loaded");

			// Creates the response.
			Document xml = ack();

			// The current root element (initially is the document element of
			// the response).
			Element root = xml.getDocumentElement();

			// Gets the analysis engine type system.
			TypeSystem ts = ae.newCAS().getTypeSystem();

			// Performs a deep-first traversal of the type system.
			// The support stack, whose layout is:
			// <bottom> --> null | child(N) | child(N-1) | ... | child(2) |
			// child(1) <-- <head>
			// when it pops the null value, it has visited one type children, so
			// it must go upward in the DOM representing the response.
			Stack<Type> stack = new Stack<Type>();

			// Pushes the TOP type.
			stack.push(ts.getTopType());

			// Until the stack isn't empty.
			while (!stack.empty()) {
				// Gets the type at the top.
				Type t = stack.pop();
				if (t == null) { // if the current value is null, go upward.
					// the root is the current root's parent element.
					root = (Element) root.getParentNode();
				} else {
					// Appends a new child element, which becomes the new root.
					root = (Element) root
							.appendChild(xml.createElement("Type"));

					// Stores the type fully qualified name.
					root.setAttribute("name", t.getName());

					// Pushes the null value.
					stack.push(null);

					@SuppressWarnings("unchecked")
					Iterator<Type> it = new ReverseListIterator(ts
							.getDirectSubtypes(t)); // pushes the children in
													// reverse order, to allow
													// in order traversal
					while (it.hasNext())
						stack.push(it.next());
				}
			}

			// Returns the response.
			return xml;
		} catch (Exception e) { // if an error occurred.
			e.printStackTrace();
			return servletUtilities.documentError(e.toString());
		}
	}

	/** The servlets utilities used to format the error messages. */
	private ServletUtilities servletUtilities;

	/** The loaded analysis engine (or null). */
	private AnalysisEngine ae;

	/** The component identifier (or null). */
	private String componentID;

	/** The instance UUID. */
	private UUID instanceUUID;
}
